// geohash.js
// Geohash library for Javascript
// (c) 2008 David Troy
// Distributed under the MIT License

const BITS = [16, 8, 4, 2, 1];

const BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz";
const NEIGHBORS = {
    right: {
        even: "bc01fg45238967deuvhjyznpkmstqrwx"
    },
    left: {
        even: "238967debc01fg45kmstqrwxuvhjyznp"
    },
    top: {
        even: "p0r21436x8zb9dcf5h7kjnmqesgutwvy"
    },
    bottom: {
        even: "14365h7k9dcfesgujnmqp0r2twvyx8zb"
    }
};

const BORDERS = {
    right: {
        even: "bcfguvyz"
    },
    left: {
        even: "0145hjnp"
    },
    top: {
        even: "prxz"
    },
    bottom: {
        even: "028b"
    }
};

NEIGHBORS.bottom.odd = NEIGHBORS.left.even;
NEIGHBORS.top.odd = NEIGHBORS.right.even;
NEIGHBORS.left.odd = NEIGHBORS.bottom.even;
NEIGHBORS.right.odd = NEIGHBORS.top.even;

BORDERS.bottom.odd = BORDERS.left.even;
BORDERS.top.odd = BORDERS.right.even;
BORDERS.left.odd = BORDERS.bottom.even;
BORDERS.right.odd = BORDERS.top.even;

function refine_interval(interval, cd, mask) {
    if (cd & mask)
        interval[0] = (interval[0] + interval[1]) / 2;
    else
        interval[1] = (interval[0] + interval[1]) / 2;
}

function calculateAdjacent(srcHash, dir) {
    srcHash = srcHash.toLowerCase();
    var lastChr = srcHash.charAt(srcHash.length - 1);
    var type = (srcHash.length % 2) ? 'odd' : 'even';
    var base = srcHash.substring(0, srcHash.length - 1);
    if (BORDERS[dir][type].indexOf(lastChr) != -1)
        base = calculateAdjacent(base, dir);
    return base + BASE32[NEIGHBORS[dir][type].indexOf(lastChr)];
}

function decodeGeoHash(geohash) {
    var is_even = 1;
    var lat = [];
    var lon = [];
    lat[0] = -90.0;
    lat[1] = 90.0;
    lon[0] = -180.0;
    lon[1] = 180.0;
    lat_err = 90.0;
    lon_err = 180.0;

    for (i = 0; i < geohash.length; i++) {
        c = geohash[i];
        cd = BASE32.indexOf(c);
        for (j = 0; j < 5; j++) {
            mask = BITS[j];
            if (is_even) {
                lon_err /= 2;
                refine_interval(lon, cd, mask);
            } else {
                lat_err /= 2;
                refine_interval(lat, cd, mask);
            }
            is_even = !is_even;
        }
    }
    lat[2] = (lat[0] + lat[1]) / 2;
    lon[2] = (lon[0] + lon[1]) / 2;

    return {
        latitude: lat,
        longitude: lon
    };
}

function decideHashLength(distance) {
    if (distance < 20) {
        return 8;
    } else if (distance < 76) {
        return 7;
    } else if (distance < 610) {
        return 6;
    } else if (distance < 2400) {
        return 5;
    } else if (distance < 19500) {
        return 4;
    } else if (distance < 78000) {
        return 3;
    } else if (distance < 630000) {
        return 2;
    } else {
        return 1;
    }
}

function getDistance(from, to, decimal = 2) {
    let earth_radius = 6370.996

    function rad(d) {
        return d * Math.PI / 180
    }
    let radFromLat = rad(from.latitude)
    let radToLat = rad(to.latitude)
    let radLatDistance = radFromLat - radToLat
    let radLngDistance = rad(from.longitude) - rad(to.longitude)
    let result = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(radLatDistance / 2), 2) + Math.cos(radFromLat) * Math.cos(radToLat) * Math.pow(Math.sin(radLngDistance / 2), 2)))
    result = result * earth_radius
    return parseFloat((result * 1000).toFixed(decimal))
}

function encodeGeoHash(latitude, longitude) {
    var is_even = 1;
    var i = 0;
    var lat = [];
    var lon = [];
    var bit = 0;
    var ch = 0;
    var precision = 12;
    geohash = "";

    lat[0] = -90.0;
    lat[1] = 90.0;
    lon[0] = -180.0;
    lon[1] = 180.0;

    while (geohash.length < precision) {
        if (is_even) {
            mid = (lon[0] + lon[1]) / 2;
            if (longitude > mid) {
                ch |= BITS[bit];
                lon[0] = mid;
            } else
                lon[1] = mid;
        } else {
            mid = (lat[0] + lat[1]) / 2;
            if (latitude > mid) {
                ch |= BITS[bit];
                lat[0] = mid;
            } else
                lat[1] = mid;
        }

        is_even = !is_even;
        if (bit < 4)
            bit++;
        else {
            geohash += BASE32[ch];
            bit = 0;
            ch = 0;
        }
    }
    return geohash;
}

module.exports = {
    decodeGeoHash: decodeGeoHash,
    encodeGeoHash: encodeGeoHash,
    decideHashLength: decideHashLength,
    getDistance
}